#!/bin/sh
#
# units - Units test harness for ctags
#
# Copyright (C) 2014 Masatake YAMATO
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
if test -n "${ZSH_VERSION+set}"; then
    set -o SH_WORD_SPLIT
    set +o NOMATCH
fi

#
# Global Parameters
#
SHELL=/bin/sh
CTAGS=./ctags
READTAGS=./readtags
WITH_TIMEOUT=
WITH_VALGRIND=
COLORIZED_OUTPUT=yes
[ -f /dev/stdout ] && COLORIZED_OUTPUT=no
CATEGORIES=
UNITS=
LANGUAGES=
PRETENSE_OPTS=
RUN_SHRINK=
QUIET=
SHOW_DIFF_OUTPUT=
VALIDATORS=

#
# Internal variables and constants
#
_CMDLINE=
_CMDLINE_FOR_SHRINKING=
_PREPERE_ENV=
_FEATURE_LIST=
readonly _DEFAULT_CATEGORY=ROOT
readonly _TIMEOUT_EXIT=124
readonly _VG_TIMEOUT_FACTOR=10
readonly _VALGRIND_EXIT=58
readonly _BASH_INTERRUPT_EXIT=59
readonly _LINE_SPLITTER=$(if type dos2unix > /dev/null 2>&1; then echo "dos2unix"; else echo "cat"; fi)
readonly _STDERR_OUTPUT_NAME="STDERR.tmp"
readonly _DIFF_OUTPUT_NAME="DIFF.tmp"
readonly _NOISE_REPORT_MAX_COLUMN=50
readonly _VALIDATION_EXIT_INVALID=2
readonly _NOOP_VALIDATOR="NONE"
readonly _KNOWN_INVALIDATION_VALIDATOR="KNOWN-INVALIDATION"

_RUNNABLE_VALIDATORS=
_UNAVAILABLE_VALIDATORS=

#
# Result files and results
#
readonly R_PASSED="_PASSED.result"
readonly R_FIXED="_FIXED.result"
readonly R_FAILED_BY_STATUS="_FAILED_BY_STATUS.result"
readonly R_FAILED_BY_DIFF="_FAILED_BY_DIFF.result"
readonly R_SKIPPED_BY_FEATURES="_SKIPPED_BY_FEATURES.result"
readonly R_SKIPPED_BY_LANGUAGES="_SKIPPED_BY_LANGUAGES.result"
readonly R_SKIPPED_BY_ILOOP="_SKIPPED_BY_ILOOP.result"
readonly R_KNOWN_BUGS="_KNOWN_BUGS.result"
readonly R_FAILED_BY_TIMEED_OUT="_FAILED_BY_TIMEED_OUT.result"
readonly R_BROKEN_ARGS_CTAGS="_BROKEN_ARGS_CTAGS.result"
readonly R_VALGRIND="_VALGRIND.result"
L_PASSED=
L_FIXED=
L_FAILED_BY_STATUS=
L_FAILED_BY_DIFF=
L_SKIPPED_BY_FEATURES=
L_SKIPPED_BY_LANGUAGES=
L_SKIPPED_BY_ILOOP=
L_KNOWN_BUGS=
L_FAILED_BY_TIMEED_OUT=
L_BROKEN_ARGS_CTAGS=
L_VALGRIND=

V_VALID=0
V_INVALID=0
V_SKIP_VALIDATOR_UNAVAILABLE=0
V_SKIP_KNOWN_INVALIDATION=0

#
# TODO
#
#  * write new class 'r' (category directory) to units.rst.
#  * write new class 'v' (skip the checking by valgrind) to units.rst.
#
action_help ()
{
    cat <<EOF
Usage:
	$(help_help)

	$(help_run)

	$(help_clean)

	$(help_fuzz)

	$(help_shrink)

	$(help_noise)

	$(help_tmain)

	$(help_chop)

	$(help_validate_input)

	$(help_clean_tmain)
EOF
}

help_help()
{
    echo "$0 help|--help"
}

ERROR ()
{
    local status_="$1"
    local msg="$2"
    shift 2
    echo "$msg" 1>&2
    exit $status_
}

line()
{
    local c=${1:--}
    local no_newline="${2}"
    local i=0
    while [ $i -lt 60 ]; do
	printf "%c" "$c"
	i=$(( i + 1 ))
    done

    if ! [ "${no_newline}" = "--no-newline" ]; then
	echo
    fi
}

count_list ()
{
    echo $#
}

member_p ()
{
    local elt="$1"
    shift
    local x

    for x in "$@"; do
	if [ "$x" = "$elt" ]; then
	    return 0
	fi
    done

    return 1
}

clean_tcase ()
{
    local d="$1"
    local bundles="$2"
    local b

    if [ -d "$d" ]; then
	if [ -f "${bundles}" ]; then
	    while read b; do
		rm -rf "${b}"
	    done < "${bundles}"
	    rm ${bundles}
	fi
	rm -f "$d"/*.tmp "$d"/*.TMP
    fi
}

check_availability()
{
    local cmd="$1"
    shift
    type "${cmd}" > /dev/null 2>&1 || ERROR 1 "${cmd} command is not available"
}

check_units ()
{
    local name="$1"
    local category="$2"
    shift 2
    local u

    for u in "$@"; do
	if echo "${u}" | grep -q /; then
	    if [ "${u%/*}" = "${category}" ] && [ "${u#*/}" = "${name}" ]; then
		return 0
	    fi
	elif [ "${u}" = "${name}" ]; then
	    return 0
	fi
    done
    return 1
}

init_features()
{
    _FEATURE_LIST=$( ${CTAGS} --quiet --options=NONE \
			      --list-features --with-list-header=no \
			      2> /dev/null \
		| "${_LINE_SPLITTER}" \
		| cut -f 1 -d ' ')
}

check_features()
{
    local flag="$1"
    local ffile
    local feature

    if [ "${flag}" = "-f" ]; then
	ffile="$2"
    elif [ "${flag}" = "-e" ]; then
	feature="$2"
    fi
    shift 2

    local f
    local found
    local found_unexpectedly
    local expected;


    for expected in $([ -f "$ffile" ] && cat "$ffile") ${feature}; do
	    found=no
	    found_unexpectedly=no
	    for f in ${_FEATURE_LIST} ; do
		[ "$expected" = "$f" ] && found=yes
		[ "$expected" = '!'"$f" ] && found_unexpectedly=yes
	    done
	    if [ "${found_unexpectedly}" = yes ]; then
		echo "$expected"
		return 1
	    elif ! [ "$found" = yes ]; then
		echo "$expected"
		return 1
	    fi
    done

    return 0
}

check_languages()
{
    local lfile="$1"
    shift

    local l
    local found
    local expected;


    #
    # TODO: consider the value of LANGUAGES
    #
    while read expected; do
	    found=no
	    for l in $( ${_CMDLINE} --list-languages 2>/dev/null | "${_LINE_SPLITTER}" |sed -e 's/ //' ); do
		[ "$expected" = "$l" ] && found=yes
	    done
	    if ! [ "$found" = yes ]; then
		echo "$expected"
		return 1
	    fi
    done < "$lfile"

    return 0
}

decorate ()
{
    local decorator="$1"
    local msg="$2"

    case "$decorator" in
	red)    decorator=31 ;;
	green)  decorator=32 ;;
	yellow) decorator=33 ;;
	*) ERROR 1 "INTERNAL ERROR: wrong run_result function: $f"
    esac

    if [ "${COLORIZED_OUTPUT}" = 'yes' ]; then
	printf '%b\n' "\033[${decorator}m${msg}\033[39m"
    else
	printf '%b\n' "${msg}"
    fi
}

run_result ()
{
    local result_type="$1"
    local msg="$2"
    local output="$3"
    shift 3
    local f="run_result_${result_type}"
    local tmp

    type "$f" > /dev/null 2>&1 || ERROR 1 \
	"${msg}INTERNAL ERROR: wrong run_result function: $f"

    "$f" "${msg}" "$@"

    tmp="${COLORIZED_OUTPUT}"
    COLORIZED_OUTPUT=no
    "$f" "${msg}" "$@" > "${output}"
    COLORIZED_OUTPUT="${tmp}"
}

run_result_skip ()
{
    local msg="$1"
    shift 1

    if [ -n "$1" ]; then
	printf '%b%b\n' "${msg}" $(decorate yellow "skipped")" ($1)"
    else
	printf '%b%b\n' "${msg}" $(decorate yellow "skipped")
    fi
}

run_result_error ()
{
    local msg="$1"
    shift 1

    if [ ! -n "$1" ]; then
	printf '%b%b\n' "${msg}" $(decorate red "failed")
    else
	printf '%b%b\n' "${msg}" $(decorate red "failed")" ($1)"
    fi
}

run_result_ok ()
{
    local msg="$1"
    shift 1

    if [ ! -n "$1" ]; then
	printf '%b%b\n' "${msg}" $(decorate green "passed")
    else
	printf '%b%b\n' "${msg}" $(decorate green "passed")" ($1)"
    fi
}

run_result_known_error ()
{
    local msg="$1"
    shift 1

    printf '%b%b\n' "${msg}" $(decorate yellow "failed")" (KNOWN bug)"
}

run_shrink ()
{
    local cmdline_template="$1"
    local input="$2"
    local output="$3"
    local lang="$4"
    shift 4

    echo "Shrinking ${input} as ${lang}"
    shrink_main "${cmdline_template}" "${input}" "${output}"  1 yes
}

# filters out the directory prefix in a ctags input
ctags_basename_filter_regex='s%\(^[^	]\{1,\}	\)\(/\{0,1\}\([^/	]\{1,\}/\)*\)%\1%'
ctags_basename_filter()
{
    sed "${ctags_basename_filter_regex}"
}

# About "input" in the expression, see units.py.
etags_basename_filter_regex='s%.*\/\(input[-._][[:print:]]\{1,\}\),\([0-9]\{1,\}$\)%\1,\2%'
etags_basename_filter()
{
    sed "${etags_basename_filter_regex}"
}

xref_basename_filter_regex='s%\(.*[[:digit:]]\{1,\} \)\([^ ]\{1,\}[^ ]\{1,\}\)/\([^ ].\{1,\}.\{1,\}$\)%\1\3%'
xref_basename_filter()
{
    sed "${xref_basename_filter_regex}"
}

json_basename_filter_regex='s%\("path": \)"[^"]\{1,\}/\([^/"]\{1,\}\)"%\1"\2"%'
json_basename_filter()
{
    sed "${json_basename_filter_regex}"
}

run_record_cmdline ()
{
    local ffilter="$1"
    local ocmdline="$2"

    printf "%s\n%s \\\\\n| %s \\\\\n| %s\n"  \
	"${_PREPERE_ENV}" \
	"${_CMDLINE}" \
	"sed '${tags_basename_filter_regex}'" \
	"${ffilter}" \
	> "${ocmdline}"
}

#
# All files and directories other than input.*, expected.tags,
# args.ctags, README*, features, languages, and filters under srcdir
# are copied to builddir. These copied files are called bundles.
#
prepare_bundles ()
{
    local from=$1
    local to=$2
    local obundles=$3
    local src
    local dist

    for src in ${from}/*; do
	if [ "${from}"'/*' = "${src}" ]; then
	    break
	fi
	case "${src##*/}" in
	    input.*)
		continue
		;;
	    expected.tags*)
		continue
		;;
	    README*)
		continue
		;;
	    features|languages|filters)
		continue
		;;
	    args.ctags)
		continue
		;;
	    *)
		dist="${to}/${src##*/}"
		if ! cp -a "${src}" "${to}"; then
		    ERROR 1 "failure in copying bundle file \"${src}\" to \"${to}\""
		else
		    echo ${dist} >> ${obundles}
		fi
		;;
	esac
    done
}

direq ()
{
    [ "$(cd ${1} && pwd)" = "$(cd ${2} && pwd)" ]
    return $?
}

anon_normalize ()
{
    local ctags=$1
    local input_actual

    if [ -n "$2" ]; then
	input_actual=$2
	shift 2

	# TODO: "Units" should not be hardcoded.
	local input_expected="./Units${input_actual#*/Units}"

	local actual=$(${CTAGS} --quiet --options=NONE --_anonhash="${input_actual}")
	local expected=$(${CTAGS} --quiet --options=NONE --_anonhash="${input_expected}")

	sed -e s/${actual}/${expected}/g | anon_normalize "${ctags}" "$@"
    else
	cat
    fi
}

run_tcase ()
{
    local input="$1"
    local t="$2"
    local name="$3"
    local class="$4"
    local category="$5"
    local build_t="$6"
    shift 6
    # The rest of arguments ($@) are extra inputs

    # I violate the naming convention of build_* to reduce typing
    local o=${build_t}

    local fargs="$t/args.ctags"
    local ffeatures="$t/features"
    local flanguages="$t/languages"
    local ffilter="$t/filter"

    #
    # tags-e if for etags output(-e). TAGS is good
    # suffix but foo.tags and foo.TAGS may be the same on Windows.
    # tags-x is for cross reference output(-x).
    # tags-json is for json output.
    #
    # fexpected must be set even if none of
    # expected.{tags,tags-e,tags-x,tags-json} exits.
    #
    local fexpected="$t/expected.tags"
    local output_type=ctags
    local output_label=
    local output_tflag=
    local output_feature=
    local output_lang_extras=

    if [ -f "$t/expected.tags" ]; then
	:
    elif [ -f "$t/expected.tags-e" ]; then
	fexpected=$t/expected.tags-e
	output_type=etags
	output_label=/${output_type}
	output_tflag="-e  --tag-relative=no"
    elif [ -f "$t/expected.tags-x" ]; then
	fexpected=$t/expected.tags-x
	output_type=xref
	output_label=/${output_type}
	output_tflag=-x
    elif [ -f "$t/expected.tags-json" ]; then
	fexpected=$t/expected.tags-json
	output_type=json
	output_label=/${output_type}
	output_tflag="--output-format=json"
	output_feature=json
    fi

    if [ $# -gt 0 ]; then
	output_lang_extras=" (multi inputs)"
    fi

    [ -x "$ffilter" ] || ffilter=cat

    #
    # All generated file must have suffix ".tmp".
    #
    local ostderr="$o/${_STDERR_OUTPUT_NAME}"
    local orawout="$o/RAWOUT.tmp"
    local ofiltered="$o/FILTERED.tmp"
    local odiff="$o/${_DIFF_OUTPUT_NAME}"
    local ocmdline="$o/CMDLINE.tmp"
    local ovalgrind="$o/VALGRIND.tmp"
    local oresult="$o/RESULT.tmp"
    local oshrink_template="$o/SHRINK-%s.tmp"
    local obundles="$o/BUNDLES"
    local oshrink

    local guessed_lang
    local guessed_lang_no_slash
    local cmdline_template
    local timeout_value
    local tmp
    local msg

    local broke_args_ctags

    #
    # Filtered by UNIT
    #
    if [ -n "${UNITS}" ]; then
	check_units "${name}" "${category}" ${UNITS} || return 1
    fi

    #
    # Build _CMDLINE
    #
    _CMDLINE="${CTAGS} --verbose --options=NONE $PRETENSE_OPTS --optlib-dir=+$t/optlib -o -"
    [ -f "${fargs}" ] && _CMDLINE="${_CMDLINE} --options=${fargs}"

    if [ -f "${fargs}" ] && ! ${_CMDLINE} --_force-quit=0 > /dev/null 2>&1; then
	broke_args_ctags=1
    fi

    #
    # Filtered by LANGUAGES
    #
    guessed_lang=$( ${_CMDLINE} --print-language "$input" 2>/dev/null | sed -n 's/^.*: //p')
    if [ -n "${LANGUAGES}" ]; then
	member_p "${guessed_lang}" ${LANGUAGES} || return 1
    fi
    guessed_lang_no_slash=$(echo "${guessed_lang}" | tr '/' '-')
    oshrink=$(printf "${oshrink_template}" "${guessed_lang_no_slash}")

    clean_tcase "${o}" "${obundles}"
    mkdir -p "${o}"
    if ! direq "${o}" "${t}"; then
	prepare_bundles ${t} ${o} "${obundles}"
    fi


    msg=$(printf '%-60s' "Testing ${name} as ${guessed_lang}${output_lang_extras}${output_label}")

    if tmp=$( ( [ -n "${output_feature}" ] && ! check_features -e "${output_feature}" ) ||
	      ( [ -f "${ffeatures}" ] && ! check_features -f "${ffeatures}" ) ); then
	echo "${category}/${name}" >> ${R_SKIPPED_BY_FEATURES}
	case "${tmp}" in
	    !*) run_result skip "${msg}" "${oresult}" "unwanted feature \"${tmp#?}\" is available";;
	    *)  run_result skip "${msg}" "${oresult}" "required feature \"${tmp}\" is not available";;
	esac
	return 1
    elif [ -f "${flanguages}" ] && ! tmp=$(check_languages "${flanguages}"); then
	echo "${category}/${name}" >> ${R_SKIPPED_BY_LANGUAGES}
	run_result skip "${msg}" "${oresult}" "required language parser \"$tmp\" is not available"
	return 1
    elif [ "$WITH_TIMEOUT" = 0 ] && [ "${class}" = 'i' ]; then
	echo "${category}/${name}" >> ${R_SKIPPED_BY_ILOOP}
	run_result skip "${msg}" "${oresult}" "may cause an infinite loop"
	return 1
    elif [ "$broke_args_ctags" = 1 ]; then
	run_result error "${msg}" '/dev/null' "broken args.ctags?"
	echo "${category}/${name}/" >> ${R_BROKEN_ARGS_CTAGS}
	return 1
    fi

    cmdline_template="${_CMDLINE} --language-force=${guessed_lang} %s > /dev/null 2>&1"
    _CMDLINE="${_CMDLINE} ${output_tflag} ${input} $@"

    timeout_value=$WITH_TIMEOUT
    if [ "$WITH_VALGRIND" = yes ]; then
	_CMDLINE="valgrind --leak-check=full --error-exitcode=${_VALGRIND_EXIT} --log-file=${ovalgrind} ${_CMDLINE}"
	timeout_value=$(( timeout_value * ${_VG_TIMEOUT_FACTOR} ))
    fi

    if ! [ "$timeout_value" = 0 ]; then
	_CMDLINE="timeout $timeout_value ${_CMDLINE}"
    fi

    {
	(
	    #
	    # When a launched process is exited abnormally, the parent shell reports it
	    # to stderr: See j_strsignal function call in wait_for in bash-4.2/nojobs.c.
	    # This becomes noise; close the stderr of subshell.
	    #
	    exec  2>&-;
	    #
	    # The original bug report(#1100 by @techee):
	    # --------------------------------------------------------------------------
	    # When running
	    #
	    #     make units VG=1
	    #
	    # one cannot stop its execution by pressing Ctrl+C and
	    # there doesn't seem to be any way (except for looking at
	    # processes which run and killing them) to stop its
	    # execution.
	    #
	    trap "exit ${_BASH_INTERRUPT_EXIT}" INT;
	    ${_CMDLINE} 2> "${ostderr}" > "${orawout}"
	)
	tmp="$?"
	run_record_cmdline "${ffilter}" "${ocmdline}"
    }
    if [ "$tmp" != 0 ]; then
	if [ "${tmp}" = "${_BASH_INTERRUPT_EXIT}" ]; then
	    ERROR 1 "The execution is interrupted"
	elif ! [ "$WITH_TIMEOUT" = 0 ] && [ "${tmp}" = "${_TIMEOUT_EXIT}" ]; then
	    echo "${category}/${name}" >> ${R_FAILED_BY_TIMEED_OUT}
	    run_result error "${msg}" "${oresult}" "TIMED OUT"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    [ "${RUN_SHRINK}" = 'yes' ] \
		&& [ $# -eq 0 ] \
		&& run_shrink "${cmdline_template}" "${input}" "${oshrink}" "${guessed_lang}"
	    return 1
	elif [ "$WITH_VALGRIND" = 'yes' ] && [ "${tmp}" = "${_VALGRIND_EXIT}" ] && ! [ "${class}" = v ]; then
	    echo "${category}/${name}" >> ${R_VALGRIND}
	    run_result error "${msg}" "${oresult}" "valgrind-error"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    return 1
	elif [ "$class" = 'b' ]; then
	    echo "${category}/${name}" >> ${R_KNOWN_BUGS}
	    run_result known_error "${msg}" "${oresult}"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    [ "${RUN_SHRINK}" = 'yes' ] \
		&& [ $# -eq 0 ] \
		&& run_shrink "${cmdline_template}" "${input}" "${oshrink}" "${guessed_lang}"
	    return 0
	else
	    echo "${category}/${name}" >> ${R_FAILED_BY_STATUS}
	    run_result error "${msg}" "${oresult}" "unexpected exit status: $tmp"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    [ "${RUN_SHRINK}" = 'yes' ] \
		&& [ $# -eq 0 ] \
		&& run_shrink "${cmdline_template}" "${input}" "${oshrink}" "${guessed_lang}"
	    return 1
	fi
    elif [ "$WITH_VALGRIND" = 'yes' ] && [ "$class" = 'v' ]; then
	echo "${category}/${name}" >> ${R_FIXED}
    fi

    if ! [ -f "${fexpected}" ]; then
	clean_tcase "${o}" "${obundles}"
	if [ "$class" = 'b' ]; then
	    echo "${category}/${name}" >> ${R_FIXED}
	elif [ "$class" = 'i' ]; then
	    echo "${category}/${name}" >> ${R_FIXED}
	fi
	echo "${category}/${name}" >> ${R_PASSED}
	run_result ok "${msg}" '/dev/null' "\"expected.tags*\" not found"
	return 0
    fi

    ${output_type}_basename_filter < "${orawout}" | \
	anon_normalize "${CTAGS}" "${input}" "$@" | \
	$ffilter > "${ofiltered}"

    {
	diff -U 0 -I '^!_TAG' --strip-trailing-cr "${fexpected}" "${ofiltered}" > "${odiff}"
	tmp="$?"
    }
    if [ "${tmp}" = 0 ]; then
	clean_tcase "${o}" "${obundles}"
	if [ "${class}" = 'b' ]; then
	    echo "${category}/${name}" >> ${R_FIXED}
	elif ! [ "$WITH_TIMEOUT" = 0 ] && [ "${class}" = 'i' ]; then
	    echo "${category}/${name}" >> ${R_FIXED}
	fi

	echo "${category}/${name}" >> ${R_PASSED}
	run_result ok "${msg}" '/dev/null'
	return 0
    else
	if [ "${class}" = 'b' ]; then
	    echo "${category}/${name}" >> ${R_KNOWN_BUGS}
	    run_result known_error "${msg}" "${oresult}"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    return 0
	else
	    echo "${category}/${name}" >> ${R_FAILED_BY_DIFF}
	    run_result error "${msg}" "${oresult}" "unexpected output"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    return 1
	fi
    fi
}


failure_in_globing ()
{
    # skip if globing failed, also ignore backup files
    local file=$1
    local pat='~$|\*'
    # use [[ if it is available in the shell implementation.
    if type '[[' > /dev/null 2>&1; then
	if [[ "$file" =~ $pat ]]; then
	    return 0
	fi
    else
	if echo "$file" | grep -q '~$\|\*'; then
	    return 0
	fi
    fi
    return 1
}

run_dir ()
{
    local category="$1"
    local base_dir="$2"
    local build_base_dir="$3"
    shift 3

    local tcase_dir
    local build_tcase_dir
    local input
    local name
    local dname
    local class

    local extra_tmp
    local extra_inputs

    #
    # Filtered by CATEGORIES
    #
    if [ -n "$CATEGORIES" ] && ! member_p "${category}" $CATEGORIES; then
	return 1
    fi

    echo
    echo "Category: $category"
    line
    for input in ${base_dir}/*.[dbtiv]/input.*; do
	if failure_in_globing "$input"; then
	    continue
	fi

	dname=$(dirname $input)
	extra_inputs=$(for extra_tmp in $dname/input[-_][0-9].* \
					$dname/input[-_][0-9][-_]*.* ; do
	    if failure_in_globing "$extra_tmp"; then
		continue
	    fi
	    echo "$extra_tmp"
	done | sort)

	tcase_dir="${input%/input.*}"
	build_tcase_dir="${build_base_dir}/${tcase_dir#${base_dir}/}"
	name="${tcase_dir%.[dbtiv]}"
	name="${name##*/}"
	class="${tcase_dir#*${name}.}"
	# Run this in parallel
	run_tcase "${input}" "${tcase_dir}" "${name}" "${class}" "${category}" "${build_tcase_dir}" ${extra_inputs} &
    done
    wait

    return 0
}

run_show_diff_output ()
{
    local units_dir="$1"
    local t="$2"

    printf "	"
    line .
    sed -e 's/^.*$/	&/' ${units_dir}/${t}.*/${_DIFF_OUTPUT_NAME}
    echo
}

run_show_stderr_output ()
{
    local units_dir="$1"
    local t="$2"

    printf "	"
    line .
    sed -e 's/^.*$/	&/' ${units_dir}/${t}.*/${_STDERR_OUTPUT_NAME} | tail -50
    echo
}

run_summary ()
{
    local build_dir="${1}"
    local t

    echo
    echo "Summary (see CMDLINE.tmp to reproduce without test harness)"
    line

    printf '  %-40s' "#passed:"
    L_PASSED=$([ -f $R_PASSED ] && cat $R_PASSED)
    count_list $L_PASSED

    printf '  %-40s' "#FIXED:"
    L_FIXED=$([ -f $R_FIXED ] && cat $R_FIXED)
    count_list $L_FIXED
    for t in $L_FIXED; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
    done

    printf '  %-40s' "#FAILED (broken args.ctags?):"
    L_BROKEN_ARGS_CTAGS=$([ -f $R_BROKEN_ARGS_CTAGS ] && cat $R_BROKEN_ARGS_CTAGS)
    count_list $L_BROKEN_ARGS_CTAGS
    for t in $L_BROKEN_ARGS_CTAGS; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
    done

    printf '  %-40s' "#FAILED (unexpected-exit-status):"
    L_FAILED_BY_STATUS=$([ -f $R_FAILED_BY_STATUS ] && cat $R_FAILED_BY_STATUS)
    count_list $L_FAILED_BY_STATUS
    for t in $L_FAILED_BY_STATUS; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
	if [ "${SHOW_DIFF_OUTPUT}" = yes ]; then
	    run_show_stderr_output "${build_dir}" "${t#${_DEFAULT_CATEGORY}/}"
	fi
    done

    printf '  %-40s' "#FAILED (unexpected-output):"
    L_FAILED_BY_DIFF=$([ -f $R_FAILED_BY_DIFF ] && cat $R_FAILED_BY_DIFF)
    count_list $L_FAILED_BY_DIFF
    for t in $L_FAILED_BY_DIFF; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
	if [ "${SHOW_DIFF_OUTPUT}" = yes ]; then
	    run_show_stderr_output "${build_dir}" "${t#${_DEFAULT_CATEGORY}/}"
	    run_show_diff_output "${build_dir}" "${t#${_DEFAULT_CATEGORY}/}"
	fi
    done

    if ! [ "$WITH_TIMEOUT" = 0 ]; then
	printf '  %-40s' "#TIMED-OUT (${WITH_TIMEOUT}s)"
	L_FAILED_BY_TIMEED_OUT=$([ -f $R_FAILED_BY_TIMEED_OUT ] && cat $R_FAILED_BY_TIMEED_OUT)
	count_list $L_FAILED_BY_TIMEED_OUT
	for t in $L_FAILED_BY_TIMEED_OUT; do
	    echo "	${t#${_DEFAULT_CATEGORY}/}"
	done
    fi

    printf '  %-40s' "#skipped (features):"
    L_SKIPPED_BY_FEATURES=$([ -f $R_SKIPPED_BY_FEATURES ] && cat $R_SKIPPED_BY_FEATURES)
    count_list $L_SKIPPED_BY_FEATURES
    for t in $L_SKIPPED_BY_FEATURES; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
    done

    printf '  %-40s' "#skipped (languages):"
    L_SKIPPED_BY_LANGUAGES=$([ -f $R_SKIPPED_BY_LANGUAGES ] && cat $R_SKIPPED_BY_LANGUAGES)
    count_list $L_SKIPPED_BY_LANGUAGES
    for t in $L_SKIPPED_BY_LANGUAGES; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
    done

    if [ "$WITH_TIMEOUT" = 0 ]; then
	printf '  %-40s' "#skipped (infinite-loop):"
	L_SKIPPED_BY_ILOOP=$([ -f $R_SKIPPED_BY_ILOOP ] && cat $R_SKIPPED_BY_ILOOP)
	count_list $L_SKIPPED_BY_ILOOP
	for t in $L_SKIPPED_BY_ILOOP; do
	    echo "	${t#${_DEFAULT_CATEGORY}/}"
	done
    fi

    printf '  %-40s' "#known-bugs:"
    L_KNOWN_BUGS=$([ -f $R_KNOWN_BUGS ] && cat $R_KNOWN_BUGS)
    count_list $L_KNOWN_BUGS
    for t in $L_KNOWN_BUGS; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
    done

    if [ "$WITH_VALGRIND" = yes ]; then
	printf '  %-40s' "#valgrind-error:"
	L_VALGRIND=$([ -f $R_VALGRIND ] && cat $R_VALGRIND)
	count_list $L_VALGRIND
	for t in $L_VALGRIND; do
	    echo "	${t#${_DEFAULT_CATEGORY}/}"
	done
    fi
}

make_pretense_map ()
{
    local ifs=$IFS
    local p
    local r

    IFS=,
    for p in $1; do
	newlang=${p%/*}
	oldlang=${p#*/}

	if [ -z "$newlang" ]; then
	    ERROR 1 "newlang part of --pretend option arg is empty"
	fi
	if [ -z "$oldlang" ]; then
	    ERROR 1 "oldlang part of --pretend option arg is empty"
	fi

	r="$r --_pretend-$newlang=$oldlang"
    done
    IFS=$ifs
    echo $r
}

delete_result_files ()
{
    rm -f ${R_PASSED} ${R_FIXED} ${R_FAILED_BY_STATUS} ${R_FAILED_BY_DIFF} \
	${R_SKIPPED_BY_FEATURES} ${R_SKIPPED_BY_LANGUAGES} \
	${R_SKIPPED_BY_ILOOP} ${R_KNOWN_BUGS} ${R_FAILED_BY_TIMEED_OUT} \
	${R_BROKEN_ARGS_CTAGS}
}

action_run ()
{
    local action="$1"
    shift

    local units_dir
    local build_dir
    local d
    local build_d
    local category

    local c

    while [ $# -gt 0 ]; do
	case $1 in
	    --ctags)
		shift
		CTAGS="$1"
		shift
		;;
	    --ctags=*)
		CTAGS="${1#--ctags=}"
		shift
		;;
	    --categories)
		shift
		for c in $(echo "$1" | tr ',' ' '); do
		    if [ "$c" = "ROOT" ]; then
			CATEGORIES="$CATEGORIES ROOT"
		    else
			CATEGORIES="$CATEGORIES ${c%.r}.r"
		    fi
		done
		shift
		;;
	    --categories=*)
		for c in $(echo "${1#--categories=}" | tr ',' ' '); do
		    if [ "$c" = "ROOT" ]; then
			CATEGORIES="$CATEGORIES ROOT"
		    else
			CATEGORIES="$CATEGORIES ${c%.r}.r"
		    fi
		done
		shift
		;;
	    --units)
		shift
		UNITS=$(echo "$1" | tr ',' ' ')
		shift
		;;
	    --units=*)
		UNITS=$(echo "${1#--units=}" | tr ',' ' ')
		shift
		;;
	    --languages)
		shift
		LANGUAGES=$(echo "${1}" | tr ',' ' ')
		shift
		;;
	    --languages=*)
		LANGUAGES=$(echo "${1#--languages=}" | tr ',' ' ')
		shift
		;;
	    --with-timeout)
		shift
		WITH_TIMEOUT="$1"
		shift
		;;
	    --with-timeout=*)
		WITH_TIMEOUT="${1#--with-timeout=}"
		shift
		;;
	    --with-valgrind)
		shift
		WITH_VALGRIND=yes
		;;
	    --colorized-output)
		shift
		COLORIZED_OUTPUT="$1"
		shift
		;;
	    --colorized-output=*)
		COLORIZED_OUTPUT="${1#--colorized-output=}"
		shift
		;;
	    --run-shrink)
		RUN_SHRINK=yes
		shift
		;;
	    --show-diff-output)
		SHOW_DIFF_OUTPUT=yes
		shift
		;;
	    --with-pretense-map)
		shift
		PRETENSE_OPTS=$(make_pretense_map "$1")
		shift
		;;
	    --with-pretense-map=*)
		PRETENSE_OPTS=$(make_pretense_map "${1#--with-pretense-map=}")
		shift
		;;
	    -*)
		ERROR 1 "unknown option \"${1}\" for ${action} action"
		;;
	    *)
		units_dir="$1"
		shift
		build_dir="${1:-${units_dir}}"
		shift
		break
		;;
	esac
    done

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "$units_dir" ]; then
	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "$units_dir" ]; then
	ERROR 1 "No such directory: ${units_dir}"
    fi

    case "${build_dir}" in
	/*) ;;
	*) build_dir=$(pwd)/${build_dir} ;;
    esac

    if ! [ -d "$build_dir" ]; then
	ERROR 1 "No such directory(build_dir): ${build_dir}"
    fi

    if ! [ -f "${CTAGS}" ]; then
	ERROR 1 "no such file: ${CTAGS}"
    elif ! [ -e "${CTAGS}" ]; then
	ERROR 1 "${CTAGS} is not an executable file"
    fi

    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
    fi

    : ${WITH_TIMEOUT:=0}
    [ "$WITH_TIMEOUT" = 0 ] || check_availability timeout
    [ "$WITH_VALGRIND" = 'yes' ] && check_availability valgrind
    [ "$MSYSTEM" != '' ] && check_availability dos2unix
    check_availability grep
    check_availability diff
    init_features

    delete_result_files

    category="${_DEFAULT_CATEGORY}"
    if [ -z "$CATEGORIES" ] \
	|| ( [ -n "$CATEGORIES" ] && member_p "${category}" $CATEGORIES ); then
	run_dir "${category}" "${units_dir}" "${build_dir}"
    fi

    for d in ${units_dir}/*.r; do
	[ -d "$d" ] || continue
	category="${d##*/}"
	build_d=${build_dir}/${category}
	run_dir "${category}" "$d" "${build_d}"
    done

    run_summary "${build_dir}"
    delete_result_files

    if [ -n "${L_FAILED_BY_STATUS}" ] ||
	   [ -n "${L_FAILED_BY_DIFF}" ] ||
	   [ -n "${L_FAILED_BY_TIMEED_OUT}" ] ||
	   [ -n "${L_BROKEN_ARGS_CTAGS}" ]; then
	return 1
    else
	return 0
    fi
}

help_run ()
{
cat <<EOF
$0 run [OPTIONS] UNITS-DIR

	   Run all tests case under UNITS-DIR.

	   OPTIONS:
		--ctags CTAGS: ctags executable file for testing
		--categories CATEGORY1[,CATEGORY2,...]: run only CATEGORY* related cases.
							Category selection is done in upper
							layer than unit selection. This
							means even if a unit is specified
							with --units, it can be ignored
							is a category the units doesn't
							belong to is specified with
							--categories option.
		--colorized-output yes|no: print the result in color.
		--skip NAME: skip the case NAME (TODO: NOT IMPLEMENTED YET)
		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases.
		--units UNITS1[,UNITS2,...]: run only UNIT(S).
		--with-timeout DURATION: run a test case under timeout
					 command with SECOND.
					 0 means no timeout(default).
		--with-valgrind: run a test case under valgrind
			       If this option given, DURATION is changed to
			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
		--show-diff-output: show diff output for failed test cases in the summary.
		--with-pretense-map=NEWLANG0/OLDLANG0[,...]: make NEWLANG parser pretend
							     OLDLANG.
EOF
}

action_clean ()
{
    local action="$1"
    shift

    local units_dir=$1
    shift

    local bundles
    local b

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "$units_dir" ]; then
	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "$units_dir" ]; then
	ERROR 0 "No such directory: ${units_dir}"
    fi

    check_availability find
    check_availability rm

    for bundles in $(find "$units_dir" -name "BUNDLES"); do
	while read b; do
	    rm -rf "${b}"
	done < ${bundles}
	rm ${bundles}
    done

    rm -f $(find "$units_dir" -name '*.tmp')
    rm -f $(find "$units_dir" -name '*.TMP')
    return 0
}

help_clean ()
{
cat <<EOF
$0 clean UNITS-DIR

	   Clean all files created during units testing
EOF

}

shrink_prepare ()
{
    local output="$1"
    local input="$2"
    local start="$3"
    local len="$4"


    dd bs=1 count="${len}" skip="${start}" < "${input}" 2>/dev/null > "${output}"
}

shrink_test ()
{
    local cmdline="$1"
    local input="$2"
    local start="$3"
    local len="$4"
    local output="$5"
    local r
    local msg

    shrink_prepare "${output}" "${input}" "${start}" "${len}"
    [ "${QUIET}" = 'yes' ] || printf "[%-5u %6u]..." "${start}" $(( start + len )) 1>&2
    eval "${cmdline}" > /dev/null 2>&1
    r="$?"
    if [ "$r" -eq 0 ]; then
	msg='ok'
    elif [ "$r" -eq "${_TIMEOUT_EXIT}" ]; then
	msg='timeout'
    else
	msg='failed'
    fi
    [ "${QUIET}" = 'yes' ] || printf "%s(%u)\n" "$msg" "$r" 1>&2
    return $r
}

shrink_bisect ()
{
    local cmdline="$1"
    local input="$2"
    local len="$3"
    local output="$4"

    local end
    local start
    local step
    local delta

    local failed
    local successful

    end="${len}"
    failed="${len}"
    successful=0

    step=0
    while true; do
	delta=$((len >> (step + 1)))
	if [ "${delta}" -eq 0 ]; then
	    delta=1
	fi
	if shrink_test "${cmdline}" "${input}" 0 "${end}" "${output}"; then
	    successful="${end}"
	    if [ $(( end + 1 )) -eq "${failed}" ]; then
		end="${failed}"
		break
	    else
		end=$((end + delta))
	    fi
	else
	    failed="$end"
	    if [ $(( successful + 1 )) -eq "${end}" ]; then
		break
	    else
		end=$((end - delta))
	    fi
	fi
	step=$((step + 1 ))
    done

    len="${end}"
    start=0
    failed=0
    successful="${end}"
    step=0
    while true; do
	delta=$((len >> (step + 1)))
	if [ "${delta}" -eq 0 ]; then
	    delta=1
	fi
	if shrink_test "${cmdline}" "${input}" "${start}" $((end - start)) "${output}"; then
	    successful="${start}"
	    if [ $(( start - 1 )) -eq "${failed}" ]; then
		start=$((start - 1))
		break
	    else
		start=$((start - delta))
	    fi
	else
	    failed="${start}"
	    if [ $((successful - 1)) -eq "${start}" ]; then
		break
	    else
		start=$((start + delta))
	    fi
	fi
	step=$((step + 1))
    done

    len=$((end - start))
    shrink_prepare "${output}" "${input}" "${start}" "${len}"
    [ "${QUIET}" = 'yes' ] || echo "Minimal badinput: ${output}"
    [ "${QUIET}" = 'yes' ] || line .
    cat "${output}"
    echo

    return 0
}

shrink_main ()
{
    local cmdline_template="$1"
    local cmdline
    local input="$2"
    local len
    local output="$3"
    local duration="$4"
    local foreground="$5"

    if ! [ -f "${input}" ]; then
	ERROR 1 "No such file: ${input}"
    elif ! [ -r "${input}" ]; then
	ERROR 1 "Cannot read a file: ${input}"
    fi

    if ! cat < /dev/null > "${output}"; then
	ERROR 1 "Cannot modify a file: ${output}"
    fi

    cmdline=$(printf "${cmdline_template}" "${output}")
    if [ -n "${duration}" ] && ! [ "${duration}" -eq 0 ]; then
	if [ "${foreground}" = 'yes' ]; then
	    cmdline="timeout --foreground ${duration} ${cmdline}"
	else
	    cmdline="timeout ${duration} ${cmdline}"
	fi
    fi

    len=$(stat -c %s "${input}")

    if shrink_test "${cmdline}" "${input}" 0 "${len}" "${output}"; then
	printf "the target command line exits normally against the original input\n" 1>&2
	return 1
    fi

    if ! shrink_test "${cmdline}" "${input}" 0 0 "${output}"; then
	printf "the target command line exits abnormally against the empty input\n" 1>&2
	return 1
    fi

    shrink_bisect "${cmdline}" "${input}" "${len}" "${output}"
}

action_shrink ()
{
    local action="$1"
    shift

    local cmdline_template
    local input
    local output

    local timeout
    local duration
    local foreground


    while [ $# -gt 0 ]; do
	case $1 in
	    --timeout)
		shift
		duration=$1
		shift
		;;
	    --timeout=*)
		duration="${1#--timeout=}"
		shift
		;;
	    --foreground)
		foreground=yes
		shift
		;;
	    --quiet)
		QUIET=yes
		shift
		;;
	    -*)
		ERROR 1 "unknown option \"${1}\" for ${action} action"
		;;
	    *)
		break
		;;
	    esac
    done

    if [ $# -lt 3 ]; then
	ERROR 1 "too few arguments for ${action} action: $*"
    elif [ $# -gt 3 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    fi

    if [ -n "${foreground}" ] && [ -z "${duration}" ]; then
	ERROR 1 "--foreground option is meaningful only if --timeout option is specified."
    fi

    cmdline_template=$1
    input=$2
    output=$3
    shift 3

    shrink_main "${cmdline_template}" "${input}" "${output}" ${duration} ${foreground}
    return $?
}

help_shrink ()
{
cat <<EOF
$0 shrink [OPTIONS] CMD_TEMPLATE INPUT OUTPUT

	   Shrink the input while the execution of CMD_TEMPLATE is failed
	   and find minimal unwanted input.

	   OPTIONS:
		--timeout N: Run CMD under timeout command with duration N
		--foreground: add --foreground option to timeout command.
			      can be used with --timeout option.
	   EXAMPLES:
		misc/units shrink "u-ctags -o - %s" original-input.js  /tmp/anyname.js
EOF
}
#action_shrink shrink --timeout=1 --foreground "./a.out  < %s" input.txt output.txt

fuzz_shrink ()
{
    local cmdline_template="$1"
    local input="$2"
    local output="$3"
    local lang="$4"
    shift 4

    [ "${QUIET}" = 'yes' ] || {
	echo "Shrinking ${input} as ${lang}"
	line .
    }
    shrink_main "${cmdline_template}" "${input}" "${output}"  1 yes
}

fuzz_lang_file ()
{
    local lang="$1"
    local file="$2"
    shift 2
    local r

    local dir="${file%/*}"
    local ovalgrind="${dir}/VALGRIND-${lang}.tmp"
    local ocmdline="${dir}/CMDLINE-${lang}.tmp"
    local oshrink="${dir}/SHRINK-${lang}.tmp"

    local cmdline
    local cmdline_for_shirking

    rm -f "${ovalgrind}" "${ocmdline}" "${oshrink}"


    if [ "${WITH_VALGRIND}" = 'yes' ]; then
	cmdline=$( printf "${_CMDLINE} --language-force=${lang} ${file}" "${ovalgrind}" )
    else
	cmdline="${_CMDLINE} --language-force=${lang} ${file}"

    fi
    cmdline_for_shirking="${_CMDLINE_FOR_SHRINKING} --language-force=${lang} %s"


    [ "${QUIET}" = 'yes' ] || printf "."
    echo "${cmdline}" > "${ocmdline}"
    ${cmdline} > /dev/null
    r=$?

    case $r in
	0)
	    rm -f "${ovalgrind}" "${ocmdline}"
	    return 0
	    ;;
	${_TIMEOUT_EXIT})
	    [ "${QUIET}" = 'yes' ] || echo
	    printf '%-40s' "[timeout $lang]"
	    echo "$f"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${file}" "${oshrink}" "${lang}"
	    return 1
	    ;;
	${_VALGRIND_EXIT})
	    [ "${QUIET}" = 'yes' ] || echo
	    printf '%-40s' "[valgrind-error $lang]"
	    echo "$f"
	    return 1
	    ;;
	*)
	    [ "${QUIET}" = 'yes' ] || echo
	    printf '%-40s' "[unexpected-status($r) $lang]"
	    echo "$f"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${file}" "${oshrink}" "${lang}"
	    return 1
	    ;;
    esac

    return $r
}

fuzz_lang ()
{
    local lang="$1"
    local dir="$2"
    shift 2
    local f
    local r=0

    [ "${QUIET}" = 'yes' ] || printf '%-60s\n' "Semi-fuzzing (${lang})"
    for f in $(find "${dir}" -type f -name 'input.*'); do
	if ! fuzz_lang_file "${lang}" "${f}"; then
	    r=1
	    break
	fi
    done
    [ "${QUIET}" = 'yes' ] || echo
    return $r
}

action_fuzz ()
{
    action_fuzz_common fuzz_lang "$@"
}

action_fuzz_common ()
{
    local fn="$1"
    local action="$2"
    shift 2

    local units_dir
    local cmdline
    local lang
    local r

    while [ $# -gt 0 ]; do
	case $1 in
	    --ctags)
		shift
		CTAGS="$1"
		shift
		;;
	    --ctags=*)
		CTAGS="${1#--ctags=}"
		shift
		;;
	    --languages)
		shift
		LANGUAGES=$(echo "${1}" | tr ',' ' ')
		shift
		;;
	    --languages=*)
		LANGUAGES=$(echo "${1#--languages=}" | tr ',' ' ')
		shift
		;;
	    --quiet)
		QUIET=yes
		shift
		;;
	    --with-timeout)
		shift
		WITH_TIMEOUT="$1"
		shift
		;;
	    --with-timeout=*)
		WITH_TIMEOUT="${1#--with-timeout=}"
		shift
		;;
	    --with-valgrind)
		shift
		WITH_VALGRIND=yes
		;;
	    --colorized-output)
		shift
		COLORIZED_OUTPUT="$1"
		shift
		;;
	    --colorized-output=*)
		COLORIZED_OUTPUT="${1#--colorized-output=}"
		shift
		;;
	    --run-shrink)
		RUN_SHRINK=yes
		shift
		;;
	    -*)
		ERROR 1 "unknown option \"${1}\" for ${action} action"
		;;
	    *)
		units_dir="$1"
		shift
		break;
		;;
	esac
    done

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "$units_dir" ]; then
	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "$units_dir" ]; then
	ERROR 0 "No such directory: ${units_dir}"
    fi

    if ! [ -f "${CTAGS}" ]; then
	ERROR 1 "no such file: ${CTAGS}"
    elif ! [ -e "${CTAGS}" ]; then
	ERROR 1 "${CTAGS} is not an executable file"
    fi

    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
    fi

    : ${WITH_TIMEOUT:=2}
    [ "$WITH_TIMEOUT" = 0 ] || check_availability timeout
    [ "$WITH_VALGRIND" = 'yes' ] && check_availability valgrind
    check_availability find

    cmdline="${CTAGS} --quiet --options=NONE --kinds-all=* --fields=*"
    _CMDLINE="${cmdline} -G -o - "
    _CMDLINE_FOR_SHRINKING="${_CMDLINE}"
    if [ "$WITH_VALGRIND" = yes ]; then
	_CMDLINE="valgrind --leak-check=full --error-exitcode=${_VALGRIND_EXIT} --log-file=%s ${_CMDLINE}"
	WITH_TIMEOUT=$(( WITH_TIMEOUT * ${_VG_TIMEOUT_FACTOR} ))
    fi

    if ! [ "$WITH_TIMEOUT" = 0 ]; then
	_CMDLINE="timeout --foreground $WITH_TIMEOUT ${_CMDLINE}"
	_CMDLINE_FOR_SHRINKING="timeout --foreground 1 ${_CMDLINE_FOR_SHRINKING}"
    fi

    for lang in $( ${cmdline} --list-languages 2>/dev/null | "${_LINE_SPLITTER}" |sed -e 's/ //' ) ; do
	if [ -n "${LANGUAGES}" ] && ! member_p "${lang}" ${LANGUAGES}; then
	    continue
	fi
	"${fn}" "${lang}" "${units_dir}"
	r=$?
    done

    return $r
}

help_fuzz ()
{
cat <<EOF
$0 fuzz [OPTIONS] UNITS-DIR

	   Run all tests case under UNITS-DIR.

	   OPTIONS:
		--ctags CTAGS: ctags executable file for testing
		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases
		--quiet: don't print dots as passed test cases.
		--with-timeout DURATION: run a test case under timeout
					 command with SECOND.
					 0 means no timeout.
					 default is 1.
		--with-valgrind: run a test case under valgrind
			       If this option given, DURATION is changed to
			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
EOF
}

noise_reduce ()
{
    local input="$1"
    local len="$2"
    local pos="$3"
    shift 3

    dd bs=1 count=$pos skip=0 if="$input"
    dd bs=1 count=$(( len - pos - 1 )) skip=$(( pos + 1 )) if="$input"
}

noise_inject ()
{
    local input="$1"
    local len="$2"
    local pos="$3"
    local c="$4"
    shift 4

    dd bs=1 count=$pos skip=0 if="$input"
    printf "%c" "$c"
    dd bs=1 count=$(( len - pos )) skip=$pos if="$input"
}

noise_report_line ()
{
    local pos="$1"
    local len="$2"
    local status_="$3"
    local how="$4"

    local progress_offset=$(( pos % _NOISE_REPORT_MAX_COLUMN ))
    local nspace


    if [ $((pos + 1)) -eq "${len}" ] || [ $status_ -gt 0 ]; then
	nspace=0
	while [ $nspace -lt $(( _NOISE_REPORT_MAX_COLUMN - progress_offset - 1)) ]; do
	    printf ' '
	    nspace=$((nspace + 1))
	done
	printf " %s %d/%d" "${how}" "$pos" "${len}"
    fi
}

noise_lang_file_noisespec ()
{
    local input="$1"
    local len="$2"
    local pos="$3"
    local c="$4"
    local genfn="$5"
    local lang="$6"
    local how="$7"
    shift 7

    local msg
    if [ "${how}" = + ]; then
	msg=INJECTED
	how="${how}${c}"
    else
	msg=REDUCED
	how="${how} "
    fi

    local dir="${input%/*}"
    local onoised=$(printf "%s/NOISE-INPUT-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)
    local ocmdline=$(printf "%s/NOISE-CMDLINE-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)
    local ovalgrind=$(printf "%s/NOISE-VALGRIND-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)
    local oshrink=$(printf "%s/NOISE-SHRINK-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)

    local cmdline
    local cmdline_for_shirking
    local progress_offset
    local r

    rm -f "${ocmdline}" "${ovalgrind}" "${onoised}" "${oshrink}"
    if [ "${WITH_VALGRIND}" = 'yes' ]; then
	cmdline=$( printf "${_CMDLINE} --language-force=${lang} ${onoised}" "${ovalgrind}" )
	else
	cmdline="${_CMDLINE} --language-force=${lang} ${onoised}"
    fi
    cmdline_for_shirking="${_CMDLINE_FOR_SHRINKING} --language-force=${lang} %s"

    "${genfn}" "${input}" "${len}" "$pos" "$c" > "$onoised"  2> /dev/null

    progress_offset=$(( pos % _NOISE_REPORT_MAX_COLUMN ))
    if [ "${progress_offset}" -eq 0 ]; then
	[ $pos -gt 0 ] && printf " %s %d/%d" "${how}" "$pos" "${len}"
	echo
    fi

    echo "${cmdline}" > "${ocmdline}"
    ( exec 2>&-; ${cmdline} 2> /dev/null > /dev/null )
    r=$?
    case $r in
	    0)
	    printf 'o'
	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
	    rm "${onoised}"
	    rm -f "${ovalgrind}" "${ocmdline}"
	    ;;
	${_TIMEOUT_EXIT})
	    printf "T"
	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
	    printf '\n%-20s\n' "[timeout $lang]" "$onoised"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${onoised}" "${oshrink}" "${lang}"
	    ;;
	${_VALGRIND_EXIT})
	    printf "V"
	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
	    printf '\n%-20s %s\n' "[valgrind-error $lang]" "$onoised"
	    ;;
	*)
	    printf "!"
	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
	    printf '\n%-20s %s\n' "[unexpected-status($r) $lang]" "$onoised"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${onoised}" "${oshrink}" "${lang}"
	    ;;
    esac
    return $r
}

noise_lang_file ()
{
    local lang="$1"
    local input="$2"
    shift 2


    local cmdline
    local cmdline_for_shirking
    local len=$(stat -c %s "${input}")
    local r
    local i
    local c
    local guessed_lang

    guessed_lang=$( ${_CMDLINE_FOR_SHRINKING} --print-language "${input}" 2>/dev/null | sed -n 's/^.*: //p')
    if [ "${lang}" !=  "${guessed_lang}" ]; then
	return 0
    fi

    i=0
    c='!'
    echo "Testing cases derived from: ${input}"
    line '.' --no-newline
    while [ "$i" -lt "$len" ]; do
	if noise_lang_file_noisespec "${input}" "${len}" "$i" "$c" noise_reduce "${lang}" -; then
	    i=$(( i + 1 ))
	else
	    echo
	    return 1
	fi
    done

    for c in 'a' '0'						\
	'!' '@' '#' '$' '%' '^' '&' '*' '(' ')' '-' '=' '_'	\
	'+' '|'  '[' ']' '{' '}' '\' ';' "'" ':' '"' ',' '.'    \
	'/' '<' '>' '?' '`' '~'; do
	i=0
	while [ "$i" -lt "$len" ]; do
	    if noise_lang_file_noisespec "${input}" "${len}" "$i" "$c" noise_inject "${lang}" +; then
		i=$(( i + 1 ))
	    else
		echo
		return 1
	    fi
	done
    done
    echo
    return 0
}

noise_lang ()
{
    local lang="$1"
    local dir="$2"
    shift 2
    local f
    local r
    printf '%-60s\n' "Noised-fuzzing (${lang})"
    line '-'

    r=0
    for f in $(find "${dir}" -type f -name 'input.*'); do
	if ! noise_lang_file "${lang}" "${f}"; then
	    r=1
	    break
	fi
    done
    echo
    return $r
}

action_noise ()
{
    action_fuzz_common noise_lang "$@"
}

help_noise ()
{
cat <<EOF
$0 noise [OPTIONS] UNITS-DIR

	   Run all tests case for LANGUAGE with "noise" for
	   finding unexpected behavior like entering an
	   infinite loop.
	   Here "noise" means removing one byte from
	   somewhere file position of the original test case;
	   or adding something one byte to
	   somewhere file position of the original test case.

	   OPTIONS:
		--ctags CTAGS: ctags executable file for testing
		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases
		--quiet: don't print dots as passed test cases.
		--with-timeout DURATION: run a test case under timeout
					 command with SECOND.
					 0 means no timeout.
					 default is 1.
		--with-valgrind: run a test case under valgrind
			       If this option given, DURATION is changed to
			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
EOF
}


tmain_compare_result()
{
    local build_topdir=$1
    local f

    for f in ${build_topdir}/*/*-diff.txt; do
	if [ -f "$f" ]; then
	    echo "$f"
	    echo
	    cat "$f" | sed -e 's|.*|	&|'
	    echo
	fi
    done
    if [ -f ${build_topdir}/*/gdb-backtrace.txt ]; then
	cat ${build_topdir}/*/gdb-backtrace.txt
    fi
}

tmain_compare()
{
    local subdir=$1
    local build_subdir=$2
    local aspect=$3
    local generated
    local msg

    msg=$(printf '%-60s' "${aspect}")
    generated=${build_subdir}/${aspect}-diff.txt
    if diff -U 0 --strip-trailing-cr \
	    ${build_subdir}/${aspect}-actual.txt \
	    ${subdir}/${aspect}-expected.txt \
	    > ${generated} 2>&1; then
	run_result ok "${msg}" '/dev/null'
	rm ${generated}
	return 0
    else
	run_result error "${msg}" '/dev/null' "diff: ${generated}"
	return 1
    fi
}

failed_git_marker ()
{
    local f=$1
    local l

    if type "git" > /dev/null 2>&1; then
	l=$(git ls-files -- "$f")
	if [ -z "$l" ]; then
	    echo '<G>'
	fi
    fi
}

is_crashed ()
{
    local f=$1

    grep -q -i "core dump" "$f"
}

print_backtraces()
{
    local ctags_exe=$1
    shift 1

    local coref
    for coref in "$@"; do
	if [ -f "${coref}" ]; then
	    gdb "${ctags_exe}" -c "${coref}" -ex where -batch
	else
	    echo "no such file: ${coref}"
	fi
    done
}

CODE_FOR_IGNORING_THIS_TMAIN_TEST=77
tmain_run ()
{
    local topdir=$1
    local build_topdir=$2
    shift 2
    local units="$@"

    local subdir
    local basedir

    local test_name
    local r_failed="_failed.result"
    local failed
    local f

    local aspect
    local engine

    local r
    local a
    local status_=0
    local msg

    local need_rearrange

    if ! [ $(basename "${CTAGS}") = 'ctags' ]; then
	need_rearrange=yes
    fi

    rm -f ${r_failed}
    basedir=$(pwd)
    for subdir in ${topdir}/*.d; do
	if [ "${subdir}" = ${topdir}/'*.d' ]; then
	    return 1
	fi

	test_name=$(basename ${subdir} .d)

	if [ -n "${units}" ] && ! member_p "${test_name}" ${units}; then
	    continue
	fi

	build_subdir=${build_topdir}/$(basename ${subdir})
	if ! mkdir -p ${build_subdir}; then
	    return 1
	fi

	# Run this block in parallel
	(
	    rm -f ${build_subdir}/*-actual.txt

	    msg="\nTesting ${test_name}"
	    msg="${msg}\n$(line '-')"
	    (
		cd ${subdir}
		${SHELL} run.sh \
			 ${basedir}/${CTAGS} \
			 ${build_subdir} \
			 ${basedir}/${READTAGS}
	    ) > ${build_subdir}/stdout-actual.txt 2> ${build_subdir}/stderr-actual.txt
	    r=$?
	    echo $r > ${build_subdir}/exit-actual.txt

	    if [ -n "${need_rearrange}" ]; then
		sed -i -e 's|^'$(basename "${CTAGS}")':|ctags:|' ${build_subdir}/stderr-actual.txt
	    fi

	    if [ $r = $CODE_FOR_IGNORING_THIS_TMAIN_TEST ]; then
		msg="${msg}\n$(run_result skip "" '/dev/null' "$(cat ${build_subdir}/stdout-actual.txt)")"
		for a in ${build_subdir}/*-actual.txt; do
		    if [ -f "$a" ]; then
			rm $a
		    fi
		done
		printf "%b\n" "${msg}"
		exit
	    fi

	    if [ -f ${build_subdir}/tags ]; then
		mv ${build_subdir}/tags ${build_subdir}/tags-actual.txt
	    fi
	    for aspect in stdout stderr exit tags; do
		if [ -f ${subdir}/${aspect}-expected.txt ]; then
		    engine=compare
		    msg="${msg}\n$(tmain_${engine} ${subdir} ${build_subdir} ${aspect})"
		    if [ $? -eq 0 ]; then
			rm ${build_subdir}/${aspect}-actual.txt
		    else
			echo "${test_name}/${aspect}-${engine}$(failed_git_marker ${subdir}/${aspect}-expected.txt)" >> ${r_failed}
			if [ ${aspect} = stderr ] &&
			       is_crashed ${build_subdir}/${aspect}-actual.txt &&
			       type "gdb" > /dev/null 2>&1; then
			    print_backtraces "${basedir}/${CTAGS}" \
					     ${build_subdir}/core* \
					     > ${build_subdir}/gdb-backtrace.txt
			fi
		    fi
		elif [ -f ${build_subdir}/${aspect}-actual.txt ]; then
		    rm ${build_subdir}/${aspect}-actual.txt
		fi
	    done

	    printf "%b\n" "${msg}"
	) &
    done
    wait

    if [ -f "${r_failed}" ]; then
	status_=1
	echo
	echo Failed tests
	line '='
	failed=$(cat ${r_failed})
	for f in ${failed}; do
	    echo $f | sed -e 's|<G>| (not committed/cached yet)|'
	done
	echo

	if [ "${SHOW_DIFF_OUTPUT}" = yes ]; then
	    engine=compare
	    echo Detail "[$engine]"
	    line '-'
	    tmain_${engine}_result ${build_topdir}
	fi
	rm ${r_failed}
    fi

    return $status_
}

action_tmain ()
{
    local action="$1"
    shift
    local tmain_dir
    local build_dir

    while [ $# -gt 0 ]; do
	case $1 in
	    --ctags)
		shift
		CTAGS="$1"
		shift
		;;
	    --ctags=*)
		CTAGS="${1#--ctags=}"
		shift
		;;
	    --colorized-output)
		shift
		COLORIZED_OUTPUT="$1"
		shift
		;;
	    --colorized-output=*)
		COLORIZED_OUTPUT="${1#--colorized-output=}"
		shift
		;;
	    --with-valgrind)
		shift
		WITH_VALGRIND=yes
		;;
	    --show-diff-output)
		SHOW_DIFF_OUTPUT=yes
		shift
		;;
	    --readtags=*)
		READTAGS="${1#--readtags=}"
		shift
		;;
	    --units)
		shift
		UNITS=$(echo "$1" | tr ',' ' ')
		shift
		;;
	    --units=*)
		UNITS=$(echo "${1#--units=}" | tr ',' ' ')
		shift
		;;
	    -*)
		ERROR 1 "unknown option \"${1}\" for ${action} action"
		;;
	    *)
		tmain_dir="$1"
		shift
		build_dir=${1:-${tmain_dir}}
		if [ -n "$1" ]; then
		    shift
		fi
		break
		;;
	esac
    done

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "$tmain_dir" ]; then
	ERROR 1 "TMAIN_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "$tmain_dir" ]; then
	ERROR 1 "No such directory(tmain_dir): ${tmain_dir}"
    fi

    case "${build_dir}" in
	/*) ;;
	*) build_dir=$(pwd)/${build_dir} ;;
    esac
    if ! [ -d "$build_dir" ]; then
	ERROR 1 "No such directory(build_dir): ${build_dir}"
    fi

    if ! [ -f "${CTAGS}" ]; then
	ERROR 1 "no such file: ${CTAGS}"
    elif ! [ -e "${CTAGS}" ]; then
	ERROR 1 "${CTAGS} is not an executable file"
    fi

    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
    fi

    check_availability awk
    check_availability diff

    tmain_run ${tmain_dir} ${build_dir} ${UNITS}
    return $?
}

help_tmain ()
{
    cat <<EOF
$0 tmain [OPTIONS] TMAIN-DIR [BUILD-DIR]

	   Run tests for main part of ctags.
	   If BUILD-DIR is not given, TMAIN-DIR is reused as BUILD-DIR.

	   OPTIONS:

		--ctags CTAGS: ctags executable file for testing
		--colorized-output yes|no: print the result in color.
		--with-valgrind: (not implemented) run a test case under valgrind
		--show-diff-output: (not implemented)show diff output for failed test cases in the summary.
		--units UNITS1[,UNITS2,...]: run only Tmain/UNIT*.d (.d is not needed)
EOF
}

action_clean_tmain()
{
    local action="$1"
    shift

    local tmain_dir=$1
    shift

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "$tmain_dir" ]; then
	ERROR 1 "TMAIN_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "$tmain_dir" ]; then
	ERROR 1 "No such directory: ${tmain_dir}"
    fi

    check_availability find
    check_availability rm

    local object
    local type
    for object in stdout stderr exit tags; do
	for type in actual diff; do
	    rm -f $(find "$tmain_dir" -name ${object}-${type}.txt)
	    rm -f $(find "$tmain_dir" -name gdb-backtrace.txt)
	done
    done
    return 0
}

help_clean_tmain ()
{
    cat <<EOF
$0 clean_tmain TMAIN-DIR

	   Clean all files created during tmain testing
EOF

}

help_chop ()
{
    cat <<EOF
$0 chop|slap [OPTIONS] UNITS-DIR

	   OPTIONS:

		--ctags CTAGS: ctags executable file for testing
		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases
		--quiet: don't print dots as passed test cases.
		--with-timeout DURATION: run a test case under timeout
					 command with SECOND.
					 0 means no timeout.
					 default is 1.
		--with-valgrind: run a test case under valgrind
			       If this option given, DURATION is changed to
			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
EOF
}

action_chop ()
{
    if [ "$1" = "chop" ]; then
	action_fuzz_common chop_lang "$@"
    else
	action_fuzz_common slap_lang "$@"
    fi
}

chop_lang()
{
    chop_lang_common "tail" "$@"
}

slap_lang()
{
    chop_lang_common "head" "$@"
}

chop_lang_common ()
{
    local endpoint=$1
    shift 1

    local lang="$1"
    local dir="$2"
    shift 2
    local f
    local r

    printf '%-60s\n' "Fuzzing by truncating input from ${endpoint} (${lang})"
    line '-'

    r=0
    for f in $(find "${dir}" -type f -name 'input.*'); do
	if ! chop_lang_file "$1" "${lang}" "${f}"; then
	    r=1
	    break
	fi
    done
    echo
    return $r
}

chop_lang_file ()
{
    local endpoint=$1
    shift 1

    local lang="$1"
    local input="$2"
    shift 2

    local r
    local cmdline
    local cmdline_for_shirking
    local len=$(stat -c %s "${input}")

    local guessed_lang
    guessed_lang=$( ${_CMDLINE_FOR_SHRINKING} --print-language "${input}" 2>/dev/null | sed -n 's/^.*: //p')
    if [ "${lang}" !=  "${guessed_lang}" ]; then
	return 0
    fi

    i=0
    echo "Testing cases derived from: ${input}"
    line '.' --no-newline

    r=0
    while [ "$i" -lt "$len" ]; do
	if chop_lang_file_chopspec "${endpoint}" "${input}" "${len}" "$i" "${lang}"; then
	    i=$(( i + 1 ))
	else
	    r=1
	    break
	fi
    done
    echo
    return $r
}

chop()
{
    local endpoint=$1
    local input=$2
    local pos=$3
    local len=$4

    if [ "${endpoint}" = "tail" ]; then
	dd if=$input bs=1 count=$pos
    else
	dd if=$input bs=1 count=$((len - pos)) skip=$pos
    fi
}

chop_lang_file_chopspec()
{
    local endpoint=$1
    shift 1

    local input="$1"
    local len="$2"
    local pos="$3"
    local lang="$4"
    shift 4

    local dir="${input%/*}"
    local ochopped=$(printf "%s/CHOP-INPUT-%s.tmp" "${dir}" "$pos")
    local ocmdline=$(printf "%s/CHOP-CMDLINE-%s.tmp" "${dir}" "$pos")
    local ovalgrind=$(printf "%s/CHOP-VALGRIND-%s.tmp" "${dir}" "$pos")
    local oshrink=$(printf "%s/CHOP-SHRINK-%s.tmp" "${dir}" "$pos")

    local cmdline
    local cmdline_for_shirking
    local progress_offset
    local r

    rm -f "${ocmdline}" "${ovalgrind}" "${ochopped}" "${oshrink}"

    if [ "${WITH_VALGRIND}" = 'yes' ]; then
	cmdline=$( printf "${_CMDLINE} --language-force=${lang} ${ochopped}" "${ovalgrind}" )
    else
	cmdline="${_CMDLINE} --language-force=${lang} ${ochopped}"
    fi
    cmdline_for_shirking="${_CMDLINE_FOR_SHRINKING} --language-force=${lang} %s"

    chop "${endpoint}" "${input}" "${pos}" "${len}" > "$ochopped"  2> /dev/null

    progress_offset=$(( pos % _NOISE_REPORT_MAX_COLUMN ))
    if [ "${progress_offset}" -eq 0 ]; then
	[ $pos -gt 0 ] && printf " %d/%d" "$pos" "${len}"
	echo
    fi

    echo "${cmdline}" > "${ocmdline}"
    ( exec 2>&-; ${cmdline} 2> /dev/null > /dev/null )
    r=$?
    case $r in
	0)
	    printf 'o'
	    noise_report_line "${pos}" "${len}" "${r}" ""
	    rm "${ochopped}"
	    rm -f "${ovalgrind}" "${ocmdline}"
	    ;;
	${_TIMEOUT_EXIT})
	    printf "T"
	    noise_report_line "${pos}" "${len}" "${r}" ""
	    printf '\n%-20s\n' "[timeout $lang]" "$ochopped"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${ochopped}" "${oshrink}" "${lang}"
	    ;;
	${_VALGRIND_EXIT})
	    printf "V"
	    noise_report_line "${pos}" "${len}" "${r}" ""
	    printf '\n%-20s %s\n' "[valgrind-error $lang]" "$ochopped"
	    ;;
	*)
	    printf "!"
	    noise_report_line "${pos}" "${len}" "${r}" ""
	    printf '\n%-20s %s\n' "[unexpected-status($r) $lang]" "$ochopped"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${ochopped}" "${oshrink}" "${lang}"
	    ;;
    esac
    return $r
}

help_validate_input ()
{
    cat <<EOF
$0 validate-input [OPTIONS] UNITS-DIR VALIDATORS-DIR

	Validate the input files (only for the test cases specifying validators.)

	OPTIONS:
		--validators=validator[,...]: Validate test cases specifying
					    given validators.
		--colorized-output: yes|no: print the result in color.
EOF
}

has_validator_acceptable_name ()
{
    local validator=$1
    if [ -z "${validator}" ]; then
	return 1
    fi

    echo "${validator}" | grep -q "^[-a-zA-Z+#0-9]\+$"
}

is_validator_runnable ()
{
    local v=$1
    local d=$2
    shift 2

    "$d/"validator-"$v" is_runnable
}

update_validator_list ()
{
    local type=$1
    local v=$2

    case $type in
	runnable)
	    if ! member_p "$v" ${_RUNNABLE_VALIDATORS}; then
		_RUNNABLE_VALIDATORS="${_RUNNABLE_VALIDATORS} $v"
	    fi
	    ;;
	unavailable)
	    if ! member_p "$v" ${_UNAVAILABLE_VALIDATORS}; then
		_UNAVAILABLE_VALIDATORS="${_UNAVAILABLE_VALIDATORS} $v"
	    fi
	    ;;
	*)
	    ERROR 1 "INTERNAL ERROR: wrong validator list type: ${type}"
	    ;;
    esac
}

#
# Whether a validator can be run or not.
#
# This runs "is_runnable" subcommand of the validator.
#
validate_validator ()
{
    local v=$1
    local validators_dir=$2
    local make_error=$3

    if member_p "$v" ${_RUNNABLE_VALIDATORS}; then
	return 0
    elif member_p "$v" ${_UNAVAILABLE_VALIDATORS}; then
	return 1
    fi

    if ! has_validator_acceptable_name "$v"; then
	if [ "${make_error}" = "error" ]; then
	    ERROR 1 "Unacceptable validator name: $v"
	else
	    update_validator_list unavailable "$v"
	    return 1
	fi
    elif ! [ -f "${validators_dir}/validator-$v" ]; then
	if [ "${make_error}" = "error" ]; then
	    ERROR 1 "No such validator: $v (${validators_dir}/validator-$v)"
	else
	    update_validator_list unavailable "$v"
	    return 1
	fi
    elif ! [ -x "${validators_dir}/validator-$v" ]; then
	if [ "$make_error" = "error" ]; then
	    ERROR 1 "Not executable: $v (${validators_dir}/validator-$v)"
	else
	    update_validator_list unavailable "$v"
	    return 1
	fi
    elif ! is_validator_runnable "$v" "${validators_dir}"; then
	if [ "${make_error}" = "error" ]; then
	    ERROR 1 "$v (${validators_dir}/validator-$v) is not ready to run"
	else
	    update_validator_list unavailable "$v"
	    return 1
	fi
    fi

    update_validator_list runnable "$v"
    return 0
}

#
# Choose a validator suitable for the current context.
#
# Return value
# 0: The caller should run echo'ed validator.
#
# 1: The caller should skip the input. It means
#    - the suitable validator is not listed in VALIDATORS, or
#    - no expected.tags and no validator file exist.
#    The caller doesn't have to update any validation counters.
# 2: The caller should skip the input and update the unavailable
#    counter.
#
resolve_validator ()
{
    local validator_file=$1
    local default_validator=$2
    local has_expected_tags=$3
    local validators_dir=$4

    shift 4

    local candidate_validator
    local local_validator

    if [ -r "$validator_file" ]; then
	local_validator=$(cat "${validator_file}" | grep -v '#')
	if [ -z "${local_validator}" ]; then
	    ERROR 1 "Empty validator specfile: ${local_validator}"
	else
	    candidate_validator=${local_validator}
	fi
    elif [ "$has_expected_tags" = no ]; then
	return 1
    else
	candidate_validator=${default_validator}
    fi

    if [ -z "${candidate_validator}" ]; then
	return 2
    elif [ -z "${VALIDATORS}" ] || member_p "${candidate_validator}" ${VALIDATORS}; then
	echo "${candidate_validator}"
	if validate_validator "${candidate_validator}" "${validators_dir}" noerror; then
	    return 0
	else
	    return 2
	fi
    else
	return 1
    fi
}

#
# Report the result of validation for INPUT with decoration
# The validation result counters are updated here.
#
validate_report_one ()
{
    local input=$1
    local validator=$2
    local status=$3
    shift 3

    if [ "${validator}" = "${_NOOP_VALIDATOR}" ]; then
	return
    fi

    local i=$(basename $input)
    local d=$(basename $(dirname $input))

    printf '%-65s' "$d/$i with ${validator}"
    case "${status}" in
	valid)
	    if [ "${validator}" = "${_KNOWN_INVALIDATION_VALIDATOR}" ]; then
		printf '%b\n' $(decorate yellow "known-invalidation")
		V_SKIP_KNOWN_INVALIDATION=$(( V_SKIP_KNOWN_INVALIDATION + 1 ))
	    else
		printf '%b\n' $(decorate green "valid")
		V_VALID=$(( V_VALID + 1))
	    fi
	    ;;
	invalid)
	    printf '%b\n' $(decorate red "invalid")
	    V_INVALID=$(( V_INVALID + 1))
	    ;;
	unavailable)
	    printf '%b\n' $(decorate yellow "unavailable")
	    V_SKIP_VALIDATOR_UNAVAILABLE=$(( V_SKIP_VALIDATOR_UNAVAILABLE + 1))
	    ;;
	*)
	    ERROR 1 "INTERNAL ERROR: wrong validation status: ${status}"
	    ;;
    esac
}

#
# Run a validator for the given input
#
# This runs "validate" subcommand of the validator.
#
validate_file ()
{
    local input=$1
    local validator=$2
    local validators_dir=$3
    shift 3

    ${validators_dir}/validator-${validator} validate $input
    return $?
}

#
# Validate input files under *.[dbtiv].
#
validate_dir ()
{
    local base_dir=$1
    local default_validator=$2
    local validators_dir=$3
    shift 3

    local f
    local t

    local v0 s0 inputs0
    local v s inputs

    #
    # No expected.tags* implies the input is invalid.
    # We don't have to run any validator as far as no
    # ./validator explicitly is given.
    #
    local has_expected_tags=no
    for f in "${base_dir}"/expected.tags*; do
	[ -r "$f" ] || continue
	failure_in_globing "$f" && continue
	has_expected_tags=yes
	break
    done

    # A validator specified in ./validator is used for validating ./input.foo.
    # It will be used for validating ./input[-_]*.foo, too if ./validator[-_]*.
    # doesn't exit.
    inputs0=$(for f in "${base_dir}"/input.*; do
		  [ -r "$f" ] || continue
		  failure_in_globing "$f" && continue
		  echo $f
	      done | sort)
    v0=$(resolve_validator "${base_dir}"/validator \
			  "${default_validator}" \
			  "${has_expected_tags}" \
			  "${validators_dir}")
    s0=$?

    case "$s0" in
	0)
	    update_validator_list runnable "$v0"
	    for f in $inputs0; do
		if validate_file "$f" "$v0" "${validators_dir}"; then
		    validate_report_one "$f" "$v0" valid
		else
		    validate_report_one "$f" "$v0" invalid
		fi
	    done
	    default_validator=$v0
	    ;;
	1)
	    # no action needed
	    ;;
	2)
	    update_validator_list unavailable "$v0"
	    for f in $inputs0; do
		validate_report_one "$f" "$v0" unavailable
	    done
	    default_validator=
	    ;;
    esac

    inputs=$(for f in "${base_dir}"/input[-_][0-9].* \
		      "${base_dir}"/input[-_][0-9][-_]*.*; do
		 [ -r "$f" ] || continue
		 failure_in_globing "$f" && continue
		 echo $f
	     done | sort)

    for f in $inputs; do
	t=${f#input}; t=${t%.*}
	v=$(resolve_validator "${base_dir}"/validator"$t" \
			     "$default_validator" \
			     "$has_expected_tags" \
			     "$validators_dir")
	s=$?
	case "$s" in
	    0)
		update_validator_list runnable "$v0"
		if validate_file "$f" "$v" "${validators_dir}"; then
		    validate_report_one "$f" "$v" valid
		else
		    validate_report_one "$f" "$v" invalid
		fi
		;;
	    1)
		# no action needed
		;;
	    2)
		update_validator_list unavailable "$v0"
		validate_report_one "$f" "$v" unavailable
		;;
	esac
    done
}

#
# Validate input files under *.r.
#
validate_category ()
{
    local category=$1
    local base_dir=$2
    local validators_dir=$3
    shift 3

    local d
    local default_validator=${_NOOP_VALIDATOR}

    if [ -r "${base_dir}/validator" ]; then
	default_validator=$(cat "${base_dir}/validator" | grep -v '#')
	if [ -z "${default_validator}" ]; then
	    ERROR 1 "Empty validator specfile (in ${base_dir}/validator)"
	fi

	validate_validator "${default_validator}" "${validators_dir}" noerror
    fi

    for d in "${base_dir}"/*.[dbtiv]; do
	[ -d "$d" ] || continue
	validate_dir "$d" "${default_validator}" "${validators_dir}"
    done
}

#
# Report the summary of validations
#
validate_summary ()
{
    echo
    echo "Summary"
    line

    printf '  %-40s' "#valid:"
    printf '%b\n' $(decorate green "${V_VALID}")

    printf '  %-40s' "#invalid:"
    if [ "${V_INVALID}" = 0 ]; then
	echo "${V_INVALID}"
    else
	printf '%b\n' $(decorate red "${V_INVALID}")
    fi

    printf '  %-40s' "#skipped (known invalidation)"
    if [ "${V_SKIP_KNOWN_INVALIDATION}" = 0 ]; then
	echo 0
    else
	printf '%b\n' $(decorate yellow "${V_SKIP_KNOWN_INVALIDATION}")
    fi

    printf '  %-40s' "#skipped (validator unavailable)"
    if [ "${V_SKIP_VALIDATOR_UNAVAILABLE}" = 0 ]; then
	echo 0
    else
	local u
	printf '%b\n' $(decorate yellow "${V_SKIP_VALIDATOR_UNAVAILABLE}")

	echo
	echo "Unavailable validators"
	line
	for u in ${_UNAVAILABLE_VALIDATORS}; do
	    echo "	$u"
	done
    fi

    if [ "${V_INVALID}" = 0 ]; then
	return 0
    else
	return "${_VALIDATION_EXIT_INVALID}"
    fi
}

action_validate_input ()
{
    local action=$1
    shift

    local units_dir
    local validators_dir
    local validators

    local v
    local d

    while [ $# -gt 0 ]; do
	case $1 in
	    --validators)
		shift
		validators=$1
		shift
		;;
	    --validators=*)
		validators=${1#--validators=}
		shift
		;;
	    --colorized-output)
		shift
		COLORIZED_OUTPUT=$1
		shift
		;;
	    --colorized-output=*)
		COLORIZED_OUTPUT=${1#--colorized-output=}
		shift
		;;
	    -*)
		ERROR 1 "unknown option \"${1}\" for ${action} action"
		;;
	    *)
		units_dir=$1
		shift
		validators_dir=$1
		shift
		break;
		;;
	esac
    done

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "${units_dir}" ]; then
	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
    elif [ -z "${validators_dir}" ]; then
	ERROR 1 "VALIDATORS_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "${units_dir}" ]; then
	ERROR 1 "No such directory: ${units_dir}"
    elif ! [ -d "${validators_dir}" ]; then
	ERROR 1 "No such directory: ${units_dir}"
    fi

    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
    fi

    if [ -n "${validators}" ]; then
	VALIDATORS=$(echo "${validators}" | tr ',' ' ')
	for v in ${VALIDATORS}; do
	    validate_validator "$v" "${validators_dir}" error
	done
    fi

    for d in ${units_dir}/*.r; do
	[ -d "$d" ] || continue
	category=${d##*/}
	echo
	echo "Category: ${category}"
	line
	validate_category "${category}" "$d" "${validators_dir}"
    done

    echo
    echo "Category: ${_DEFAULT_CATEGORY}"
    line
    for d in ${units_dir}/*.[dbtiv]; do
	[ -d "$d" ] || continue
	validate_dir "$d" "${_NOOP_VALIDATOR}" "$validators_dir"
    done

    validate_summary
    return $?
}

# * Avoid issues between sed and the locale settings by overriding it using
#   LC_ALL, which takes precedence over all other locale configurations:
#   https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
#
# * Avoid unexpected pathname conversion on MSYS2.
#   https://github.com/msys2/msys2/wiki/Porting#filesystem-namespaces
prepare_environment ()
{
    _PREPERE_ENV=$(cat <<'EOF'
LC_ALL="C"; export LC_ALL
MSYS2_ARG_CONV_EXCL='--regex-;--_scopesep' export MSYS2_ARG_CONV_EXCL
EOF
)
    eval ${_PREPERE_ENV}
}

main ()
{
    if [ $# = 0 ]; then
	action_help 1>&2
	exit 1
    fi

    case $1 in
	help|-h|--help)
	    action_help
	    return 0
	    ;;
	run)
	    action_run "$@"
	    return $?
	    ;;
	clean)
	    action_clean "$@"
	    return $?
	    ;;
	fuzz)
	    action_fuzz "$@"
	    return $?
	    ;;
	noise)
	    action_noise "$@"
	    return $?
	    ;;
	tmain)
	    action_tmain "$@"
	    return $?
	    ;;
	clean-tmain)
	    action_clean_tmain "$@"
	    return $?
	    ;;
	shrink)
	    action_shrink "$@"
	    return $?
	    ;;
	chop)
	    action_chop "$@"
	    return $?
	    ;;
	slap)
	    action_chop "$@"
	    return $?
	    ;;
	validate-input)
	    action_validate_input "$@"
	    return $?
	    ;;
	*)
	    ERROR 1 "unknown action: $1"
	    ;;
    esac
}

prepare_environment
main "$@"
exit $?
